Part 3: API 보안 설계
| 항목 | 내용 |
|---|---|
| 문서명 | Part 3: API 보안 설계 (API Security Design) |
| 제품명 | DTA Wide Sleep Management Platform |
| 작성일 | 2026-02-10 |
| 적용범위 | Part 3 (백엔드 API) |
1. API 보안 개요
목표: OWASP API Top 10 준수, 안전한 RESTful API 설계
보안 원칙:
- 인증/인가 필수 (JWT Bearer Token)
- 입력 검증 (DTO + class-validator)
- Rate Limiting (Redis 기반)
- 에러 메시지 최소 정보 노출
- 관리자 엔드포인트 추가 보호
2. 인증 및 인가 (Authentication & Authorization)
2.1 JWT 기반 인증
AppToken 구조 (RS256 비대칭 서명):
// AppToken Payload (실제 구현 - app-token.guard.ts)
{
"appId": "app-uuid",
"deviceId": "device-uuid",
"jti": "token-unique-id",
"exp": 1707579000, // 30분 후 만료
"iat": 1707577200
}
인증 플로우:
토큰 무효화 정책:
비활성 감지 구현 현황:
| 항목 | 비고 |
|---|---|
| AccessToken TTL 30분 만료 | JWT exp 클레임 기반 |
| RefreshToken TTL 14일 만료 | JWT exp 클레임 기반, 장기 미사용 |
2.2 역할 기반 인가 (RBAC)
Decorator 사용:
@Controller('sleep')
@UseGuards(AppTokenGuard, FlexibleAuthGuard)
export class SleepController {
@Get('/logs')
@Roles('patient', 'clinician', 'admin')
async getSleepLogs(@CurrentUser() user: TokenPayload) {
// Patient: 본인 데이터만
// Clinician: 할당 환자만
// Admin: 모든 데이터
}
@Delete('/logs/:id')
@Roles('patient', 'admin')
async deleteSleepLog(@Param('id') id: string) {
// Patient: 본인 데이터만 삭제
// Admin: 모든 데이터 삭제 가능
}
}
2.3 앱 요청 무결성 및 진위성 검증
현재 구현된 검증 레이어:
| 검증 레이어 | 구현 상태 | 참조 파일 | 비고 |
|---|---|---|---|
| AppToken (RS256) 앱 진위성 검증 | ✅ 구현됨 | app-token.guard.ts, app-token.service.ts | JWK 기반 RS256 서명 + DB 상태 확인 |
| Device ID SHA-256 해시 검증 | ✅ 구현됨 | device-identifier.service.ts | 요청 deviceIdHash vs 서버 계산값 비교 |
| PubSub/Webhook GCP OIDC JWT 검증 | ✅ 구현됨 | pubsub.guard.ts, webhook-validation.service.ts | GCP Pub/Sub 요청 인증 |
| HMAC 기반 요청 본문 서명 검증 | ❌ 미구현 | - | AppToken 이외 추가 서명 없음 |
| 응답 본문 HMAC/서명 추가 | ❌ 미구현 | - | 응답에 서명 레이어 없음 |
3. 입력 검증 (Input Validation)
3.1 DTO 기반 검증
CreateSleepLogDto:
import { IsString, IsDateString, IsOptional, IsInt, Min, Max } from 'class-validator';
export class CreateSleepLogDto {
@IsDateString()
bedtime: string; // ISO 8601 형식
@IsDateString()
wakeTime: string;
@IsOptional()
@IsInt()
@Min(0)
@Max(480) // 최대 8시간
sol?: number; // Sleep Onset Latency (분)
@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}
ValidationPipe 설정:
// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 자동 타입 변환
whitelist: true, // DTO에 없는 속성 제거
forbidNonWhitelisted: true, // 추가 속성 시 에러
exceptionFactory: (errors) => {
// 커스텀 에러 응답
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors.map(e => ({
property: e.property,
constraints: e.constraints
}))
});
}
})
);
3.2 SQL Injection 방지
Prisma ORM 파라미터화:
// ✅ SAFE: Prisma 파라미터화된 쿼리
async findByUserId(userId: string): Promise<SleepLog[]> {
return this.prisma.sleepLog.findMany({
where: { userId: userId } // 자동 파라미터화
});
}
3.3 XSS 방지
HTML 이스케이프:
import { sanitize } from 'class-sanitizer';
export class CreateNoteDto {
@IsString()
@MaxLength(1000)
@sanitize() // HTML 태그 제거
content: string;
}
// 또는 수동 이스케이프
function syntax(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
4. Rate Limiting 및 Anti-Abuse
4.1 Rate Limiting 정책
| 엔드포인트 유형 | 제한 | 윈도우 | 초과 시 |
|---|---|---|---|
| 인증 (로그인) | 5 req/분 | 사용자당 | 429 + 1분 대기 |
| 데이터 조회 (GET) | 100 req/분 | 사용자당 | 429 + 재시도 안내 |
| 데이터 생성 (POST) | 10 req/분 | 사용자당 | 429 + 재시도 안내 |
| 관리자 API | 60 req/분 | 사용자당 | 429 + 로그 기록 |
| Public API | 1000 req/분 | IP당 | 429 + CAPTCHA |
4.2 Redis 기반 Rate Limiter
구현 (NestJS Interceptor):
@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(private readonly redis: RedisService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId || request.ip;
const key = `rate-limit:${request.path}:${userId}`;
const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, 60); // 1분 TTL
}
const limit = this.getLimit(request.path);
if (current > limit) {
throw new TooManyRequestsException(`Rate limit exceeded. Try again in 60 seconds.`);
}
return next.handle();
}
}
4.3 Brute Force 방어
로그인 실패 제한:
async login(email: string, password: string) {
const key = `login-attempts:${email}`;
const attempts = await this.redis.get(key);
if (attempts && parseInt(attempts) >= 5) {
throw new TooManyRequestsException('Too many login attempts. Try again in 15 minutes.');
}
// 비밀번호 검증
const isValid = await this.verifyPassword(email, password);
if (!isValid) {
await this.redis.incr(key);
await this.redis.expire(key, 900); // 15분
throw new UnauthorizedException('Invalid credentials');
}
// 성공 시 카운터 리셋
await this.redis.del(key);
return this.generateToken(user);
}
5. 오류 메시지 정책 (Error Message Policy)
5.1 안전한 에러 응답
프로덕션 환경:
// ✅ SAFE: 최소 정보만 노출
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "토큰이 유효하지 않습니다.",
"timestamp": "2026-02-10T12:00:00.000Z"
}
예외 필터 (Global Exception Filter):
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status = 500;
let code = 'INTERNAL_SERVER_ERROR';
let detail = '서버 내부 오류가 발생했습니다.';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
code = exceptionResponse['code'] || exception.message;
detail = exceptionResponse['detail'] || exception.message;
}
// ❌ 스택 트레이스 절대 노출 금지
// ❌ 데이터베이스 연결 정보 노출 금지
// ❌ 파일 경로 노출 금지
response.status(status).json({
code,
message: code,
detail,
timestamp: new Date().toISOString()
});
// 내부 로깅 (Cloud Logging / LoggerService)
if (status >= 500) {
this.logger.error(exception);
}
}
}
| 항목 | 구현 상태 | 비고 |
|---|---|---|
| 에러 메시지 최소화 (스택 트레이스 미노출) | ✅ 구현됨 | GlobalExceptionFilter |
5.2 HTTP 상태 코드 매핑
| 시나리오 | HTTP 상태 | 응답 코드 | 메시지 |
|---|---|---|---|
| 인증 실패 (토큰 없음) | 401 | AUTH_REQUIRED | "인증이 필요합니다." |
| 인증 실패 (토큰 만료) | 401 | TOKEN_EXPIRED | "토큰이 만료되었습니다." |
| 권한 없음 | 403 | FORBIDDEN | "접근 권한이 없습니다." |
| 리소스 없음 | 404 | NOT_FOUND | "리소스를 찾을 수 없습니다." |
| 입력 검증 실패 | 400 | VALIDATION_ERROR | "입력값이 올바르지 않습니다." |
| Rate Limit 초과 | 429 | RATE_LIMIT_EXCEEDED | "요청 한도를 초과했습니다." |
| 서버 오류 | 500 | INTERNAL_ERROR | "서버 오류가 발생했습니다." |
6. 관리자 엔드포인트 보호 (Admin Endpoint Protection)
6.1 추가 보안 계층
관리자 전용 Guard:
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
// 1. Admin 역할 확인
if (!user.roles?.includes('admin')) {
return false;
}
// 2. IP 화이트리스트 확인 (선택적)
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!this.isAllowedIp(clientIp)) {
this.logger.warn(`Admin access from unauthorized IP: ${clientIp}`);
return false;
}
// 3. 시간 기반 접근 제한 (선택적)
const hour = new Date().getUTCHours();
if (hour >= 0 && hour < 6) {
this.logger.warn(`Admin access during restricted hours: ${user.userId}`);
// 경고만, 차단하지 않음
}
return true;
}
}
6.2 민감 API 엔드포인트 목록
| 엔드포인트 | 역할 | 추가 보안 | 감사 로그 |
|---|---|---|---|
DELETE /v1/admin/users/:id | Admin | MFA 재확인 | ✅ |
POST /v1/admin/users/:id/roles | Admin | Jira 승인 | ✅ |
GET /v1/admin/audit-logs | Admin | IP 화이트리스트 | ✅ |
POST /v1/admin/system/settings | Admin | Jira 승인 | ✅ |
GET /v1/admin/users/:id/data-export | Admin, CS | 데이터 익명화 | ✅ |
7. API 보안 체크리스트
| # | 항목 | 구현 | 검증 |
|---|---|---|---|
| 1 | 모든 엔드포인트 인증 필수 (public 제외) | ✅ | API Test |
| 2 | 역할 기반 인가 (RBAC) | ✅ | API Test |
| 3 | DTO 입력 검증 (class-validator) | ✅ | API Test |
| 4 | SQL Injection 방지 (Prisma ORM) | ✅ | SAST |
| 5 | XSS 방지 (HTML 이스케이프) | ✅ | DAST |
| 6 | CSRF 방지 (SameSite Cookie) | N/A | JWT Bearer Token 사용 |
| 7 | Rate Limiting (Redis) | ✅ | API Test |
| 8 | 안전한 에러 메시지 | ✅ | API Test |
| 9 | HTTPS 강제 (TLS 1.3) | ✅ | GCP SSL |
| 10 | 민감 데이터 로깅 금지 | ✅ | 로그 샘플 검증 |
| 11 | CORS 적절한 설정 | ✅ | 설정 파일 검토 |
| 12 | API 버저닝 (v1/) | ✅ | URL 패턴 |
| 13 | 관리자 API 추가 보호 | ✅ | Admin Guard |
| 14 | 감사 로그 (민감 행위) | ✅ | 로그 샘플 |
8. OWASP API Top 10 준수
| # | OWASP API Top 10 | 구현 통제 | 상태 |
|---|---|---|---|
| 1 | Broken Object Level Authorization | userId 검증, RBAC | ✅ |
| 2 | Broken Authentication | JWT, Rate Limiting | ✅ |
| 3 | Broken Object Property Level Authorization | DTO Whitelist | ✅ |
| 4 | Unrestricted Resource Consumption | Rate Limiting, 페이지네이션 | ✅ |
| 5 | Broken Function Level Authorization | Roles Guard | ✅ |
| 6 | Unrestricted Access to Sensitive Business Flows | Rate Limiting, Block Access | ✅ |
| 7 | Server Side Request Forgery (SSRF) | URL 검증, 화이트리스트 | ✅ |
| 8 | Security Misconfiguration | 안전한 기본 설정, SAST | ✅ |
| 9 | Improper Inventory Management | Swagger 문서, API 버저닝 | ✅ |
| 10 | Unsafe Consumption of APIs | 외부 API TLS 검증, 타임아웃 | ✅ |
9. Swagger API 문서 보안
9.1 Swagger UI 접근 제한
프로덕션 환경:
// main.ts
if (process.env.NODE_ENV !== 'production') {
// 스테이징/개발 환경만 Swagger 활성화
const config = new DocumentBuilder()
.setTitle('DTA Wide API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('v1/docs', app, document);
} else {
// 프로덕션: Swagger 비활성화 또는 IP 화이트리스트
// [TODO: 프로덕션 Swagger 비활성화 확인 필요]
}
9.2 민감 정보 숨기기
@ApiProperty({
example: 'user@example.com',
description: '사용자 이메일'
// ❌ 절대 포함 금지: 실제 이메일, 토큰, 비밀번호
})
email: string;
@ApiProperty({
example: '********', // 가려진 예시
writeOnly: true, // 응답에 포함 안 함
})
@Exclude() // class-transformer로 자동 제외
password: string;
증빙 및 참조(Artifacts)
- API 보안 체크리스트 (본 문서 Section 7)
- DTO 검증 코드 -
apps/dta-wide-api/src/app/*/dto/*.dto.ts - Guards 구현 -
guards/app-token.guard.ts,guards/flexible-auth.guard.ts,guards/webhook.guard.ts - Rate Limiter 코드 -
libs/core/redis/src/lib/rate-limit.interceptor.ts - Exception Filter 코드 -
apps/dta-wide-api/src/app/filters/global-exception.filter.ts - OWASP ZAP DAST 보고서 -
reports/api-dast-latest.pdf - Swagger API 문서 -
https://staging-api.dta-wide.com/v1/docs(스테이징) - API 보안 테스트 결과 -
test-results/api-security-tests.log - Rate Limiting 테스트 - k6 스크립트 + 결과
- 감사 로그 샘플 (관리자 API) -
logs/admin-api-calls.log
| 테스트 항목 | 도구 | 결과 | 비고 |
|---|---|---|---|
| SQL Injection | OWASP ZAP | [TODO: 실행 필요] | Prisma ORM (파라미터화) |
| XSS (Reflected) | OWASP ZAP | [TODO: 실행 필요] | HTML 이스케이프 |
| Broken Authentication | Burp Suite | [TODO: 실행 필요] | AppToken RS256 + Rate Limit |
| IDOR (Insecure Direct Object Reference) | Manual Test | [TODO: 실행 필요] | userId 검증 |
| Rate Limiting | k6 Load Test | [TODO: 실행 필요] | 사용자당 100 req/min |
| CSRF | OWASP ZAP | N/A | JWT Bearer Token (Cookie 미사용) |
| 민감 정보 노출 | Manual Review | [TODO: 실행 필요] | 에러 메시지 최소화 |